#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using log4net;
using Karma.Framework.Core.Domain.Tools.Import.Layout;
using Karma.Framework.Core.Utils;

#endregion

namespace Karma.Framework.Core.Domain.Tools.Import.Data
{
    public class DefaultEntityBuilder<T> : IEntityBuilder<T>
    {
        private static readonly ILog LOG = LogManager.GetLogger(typeof (DefaultEntityBuilder<>).Name);

        #region IEntityBuilder<T> Members

        public IList<T> BuildEntities(LayoutData layoutData)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Building [{1}] entities for [{0}]", 
                    layoutData.Layout.Entity.FullName, layoutData.Data.Count));
            }
            // process entities
            var entities = new List<T>();
            var idx = 0;
            foreach (var rowData in layoutData.Data)
            {
                if (LOG.IsDebugEnabled && idx > 0 && idx % 100 == 0)
                {
                    LOG.Debug(string.Format("{0} entities built.", idx));
                }
                entities.Add((T)BuilEntity(typeof(T), rowData));
                idx++;
            }
            // process aggregates
            var aggregates = BuildAggregates(layoutData);
            // assign aggregates to its entities
            PopulateEntityAggregates(entities, aggregates);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Entities built: [{0}]", entities.ItemsToString()));
            }
            return entities;
        }

        #endregion

        private void PopulateEntityAggregates(IList<T> entities,
                                              IDictionary<EntityLayout, Dictionary<object, ArrayList>> aggregates)
        {
            foreach (var layout in aggregates.Keys)
            {
                var aggregate = aggregates[layout];
                var aggLayout = (AggregateLayout) layout;

                var field = (ReferenceField) (from f in aggLayout.Properties
                                              where f is ReferenceField && f.FieldType == typeof (T)
                                              select f).SingleOrDefault();

                var aggListType = typeof (IList<>).MakeGenericType(aggLayout.Entity);
                var aggConcreteListType = typeof (List<>).MakeGenericType(aggLayout.Entity);
                var listAddMethod = aggConcreteListType.GetMethod("Add");

                var propertyInEntity = (from p in typeof (T).GetProperties()
                                        where p.PropertyType == aggListType && p.Name.Equals(aggLayout.Name)
                                        select p).SingleOrDefault();

                var refKeyName = field.KeyRef ?? "Id";

                foreach (var entityKey in aggregate.Keys)
                {
                    var entity = (from e in entities
                                  let properties = e.GetType().GetProperties()
                                  from p in properties
                                  where p.Name == refKeyName && p.GetValue(e, new object[] {}).Equals(entityKey)
                                  select e).SingleOrDefault();
                    var list = propertyInEntity.GetValue(entity, new object[] {});
                    if (list == null)
                    {
                        list = Activator.CreateInstance(aggConcreteListType);
                        propertyInEntity.SetValue(entity, list, new object[] {});
                    }
                    foreach (var val in aggregate[entityKey])
                    {
                        listAddMethod.Invoke(list, new[] {val});
                    }
                }
            }
        }

        private IDictionary<EntityLayout, Dictionary<object, ArrayList>> BuildAggregates(LayoutData layoutData)
        {
            var aggregates = new Dictionary<EntityLayout, Dictionary<object, ArrayList>>();
            foreach (var aggregate in layoutData.Aggregates)
            {
                aggregates.Add(aggregate.Layout, new Dictionary<object, ArrayList>());

                var field = (ReferenceField) (from f in aggregate.Layout.Properties
                                              where f is ReferenceField && f.FieldType == typeof (T)
                                              select f).SingleOrDefault();
                var property = (from p in aggregate.Layout.Entity.GetProperties()
                                where p.PropertyType == typeof (T) && p.Name.Equals(field.Name)
                                select p).SingleOrDefault();
                var refPropName = field.KeyRef ?? "Id";
                var entityRefProperty = typeof (T).GetProperty(refPropName);

                foreach (var rowData in aggregate.Data)
                {
                    var instance = BuilEntity(aggregate.Layout.Entity, rowData);
                    var ownerValue = property.GetValue(instance, new object[] {});
                    var ownerKeyRefValue = entityRefProperty.GetValue(ownerValue, new object[] {});

                    if (!aggregates[aggregate.Layout].ContainsKey(ownerKeyRefValue))
                    {
                        aggregates[aggregate.Layout].Add(ownerKeyRefValue, new ArrayList());
                    }
                    aggregates[aggregate.Layout][ownerKeyRefValue].Add(instance);
                }
            }
            return aggregates;
        }

        private object BuilEntity(Type type, RowData rowData)
        {
            var entity = Activator.CreateInstance(type);
            if (rowData.PrimaryKey != null && rowData.PrimaryKey.Value != null)
            {
                entity.GetType().GetProperty("Id").SetValue(
                    entity, int.Parse(rowData.PrimaryKey.Value.ToString()), new object[] {});
            }
            foreach (var property in rowData.Data)
            {
                var propertyValue = ProcessPropertyValue(property);
                if (property.Definition is PropertyField)
                {
                    entity.GetType().GetProperty(property.Definition.Name, 
                        property.Definition.FieldType).SetValue(
                        entity, propertyValue, new object[] {});
                }
                else
                if (property.Definition is ReferenceField && !string.IsNullOrEmpty((string)property.Value))
                {
                    var reference = (ReferenceField) property.Definition;
                    var refPropName = reference.KeyRef ?? "Id";
                    var refInstance = Activator.CreateInstance(reference.FieldType);

                    refInstance.GetType().GetProperty(refPropName).SetValue(
                        refInstance, propertyValue, new object[] {});

                    entity.GetType().GetProperty(reference.Name).SetValue(
                        entity, refInstance, new object[] {});
                }
            }
            return entity;
        }

        private object ProcessPropertyValue(CellData field)
        {
            if (field.Definition is PropertyField)
            {
                if (field.Definition.FieldType.IsValueType &&
                    !field.Definition.FieldType.Equals(typeof (DateTime)) &&
                    !field.Definition.FieldType.IsEnum)
                {
                    var value = (string) field.Value;
                    if (string.IsNullOrEmpty(value))
                    {
                        return Parse(field.Definition.FieldType, "0");
                    }
                    return Parse(field.Definition.FieldType, (string) field.Value);
                }
                if (field.Definition.FieldType.Equals(typeof(DateTime)) && field.Value != null)
                {
                    var value = (string)field.Value;
                    if (string.IsNullOrEmpty(value))
                    {
                        return null;
                    }
                    return new DateTime(1899, 12, 31).AddDays(int.Parse(field.Value.ToString()));
                }
                if (field.Definition.FieldType.IsEnum)
                {
                    var value = (string)field.Value;
                    if (string.IsNullOrEmpty(value))
                    {
                        return null;
                    }
                    return Enum.Parse(field.Definition.FieldType, field.Value.ToString());
                }
                if (field.Definition.FieldType.Equals(typeof(string)))
                {
                    return field.Value;
                }

                throw new ArgumentException(string.Format("El tipo de la definicion no esta dentro de los tipos soportados [{0}].", 
                    field.Definition.FieldType));
            }
            if (field.Value == null)
            {
                return null;
            }
            var reference = (ReferenceField) field.Definition;
            if (reference.KeyRefType == typeof(string))
            {
                return field.Value == null ? "" : field.Value.ToString();
            }
            if (reference.KeyRefType == typeof(Int64))
            {
                if (field.Value == null)
                {
                    return "";
                }
                return !string.IsNullOrEmpty((string)field.Value) ? Int64.Parse((string)field.Value) : 0;
            }
            return Parse(reference.KeyRefType, (string)field.Value);
        }

        private object Parse(Type type, string expression)
        {
            var methodToInvoke = (from m in type.GetMethods(BindingFlags.Static | BindingFlags.Public)
                                  where m.Name == "Parse" && m.GetParameters().Length == 1
                                  select m).SingleOrDefault();
            return methodToInvoke.Invoke(null, new[] {expression});
        }
    }
}